一般來說,重複使用物件,可以減少不必要的資源浪費並提升效能,這在記憶體短缺的地方,會是一個很重要的原則,所以如果物件是immutable,應該盡量重複使用。
要如何避免生不必要的物件,除了使用static method之外(ex: Boolean.valueOf(String)),減少使用constructor,避免生成新的物件,也是一個方法。舉例來說,String如果是用下面constructor的方式宣告,就會生成新的物件。
String s = new String("abc");
改成下面這種方式,這樣JVM就會重複使用這個物件。
String s = "abc";
而在一些mutable的物件,如果物件裡面的資料很少被改變,也可以把一些使用到的資料或物件儲存到static變數,避免資料沒有變動,也生成新的物件。舉例來說,如果有一個Person的class,包含一個人的基本資料,現在要用生日判斷是否已經成年,如果每次都是使用Calendar的getInstance取得新的instance,會造成不必要的浪費。
import java.util.Date;
import java.util.Calendar;
public class Person {
private final String name;
private final Date birthDate;
public Person(String name, Date birthDate) {
this.name = name;
this.birthDate = birthDate;
}
public boolean isAdult() {
Calendar now = Calendar.getInstance();
int currentYear = now.get(Calendar.YEAR);
int birthYear = birthDate.getYear() + 1900;
return currentYear - birthYear >= 18;
}
}
如果可以把now設為static,就可以避免每次呼叫isAdult時,都create一個新的Calendar instance,生成不必要的物件。
import java.util.Date;
import java.util.Calendar;
public class Person {
private final String name;
private final Date birthDate;
private static Calendar now = Calendar.getInstance();
public Person(String name, Date birthDate) {
this.name = name;
this.birthDate = birthDate;
}
public boolean isAdult() {
int currentYear = now.get(Calendar.YEAR);
int birthYear = birthDate.getYear() + 1900;
return currentYear - birthYear >= 18;
}
}
雖然只呼叫一次,效能上並不會感到差異,但是如果呼叫1000次,每次都用Calendar的getInstance的方式,總共約花費20 ms,用static欄位cache Calendar instance的方式,總共約花費13 ms,可以看出大量生成物件,是真的會影響效能。
import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.time.Instant;
class Main {
public static void main(String[] args) throws ParseException {
long start, end;
start = System.currentTimeMillis();
DateFormat dateFormat2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date birthDate = dateFormat2.parse("2010-09-13 22:36:01");
Person john = new Person("John", birthDate);
System.out.println("Is adult? " + john.isAdult());
for (int i = 0; i < 1000; i++) {
john.isAdult();
}
end = System.currentTimeMillis();
System.out.println("execution time: " + (end - start) + " millisecond");
}
}
在Java 1.5版之後,開始支援autoboxing,指的是在compile的時候,Primitive type(ex: int或long)自動轉換成對應的物件(ex: Integer或Long),這個功能增加了便利性,但在無形中也產生新的物件。以下面的範例為例,i的型別是long,但sum的型別是Long,於是在相加的時候,i會被轉換成Long,這中間會生成一個新的物件,然後在跟sum相加,加完之後,雖然因為autoboxing而產生的物件結束任務了,但不會直接被回收,而是繼續佔用空間,且不斷地autoboxing生成物件,會讓程式耗費更多的時間。
import java.lang.Integer;
import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.time.Instant;
class Main {
public static void main(String[] args) throws ParseException {
long start, end;
start = System.currentTimeMillis();
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
end = System.currentTimeMillis();
System.out.println("execution time: " + (end - start) + " millisecond");
}
}
但如果一開始把sum宣告成long,就不會有autoboxing的問題,在上面的那個範例,花費了2848 ms,下面的範例只花費了685 ms,一個不注意,花費的時間居然差了約4倍。
import java.lang.Integer;
import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.time.Instant;
class Main {
public static void main(String[] args) throws ParseException {
long start, end;
start = System.currentTimeMillis();
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
end = System.currentTimeMillis();
System.out.println("execution time: " + (end - start) + " millisecond");
}
}
不過雖然避免生成不必要的物件是一件好事,但是如果重複使用物件會產生其他的問題或bug,也不一定要堅守這個原則。